/*
 *  Copyright 2011 Alexey Andreev.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package org.xthl.wiki;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.util.Version;
import org.nop.core.BeanScope;
import org.nop.core.ManagedBean;
import org.nop.diff.Change;
import org.nop.diff.DeltaEncoder;
import org.nop.diff.Patcher;
import org.nop.sql.DataManager;
import org.nop.sql.DataResult;
import org.nop.sql.ExprBuilder;
import org.nop.sql.QueryBuilder;
import org.nop.sql.SelectBuilder;
import org.nop.util.Injected;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xthl.core.api.RestrictedAccessException;
import org.xthl.core.api.UserLogic;
import org.xthl.core.data.UserSource;
import org.xthl.wiki.api.*;
import org.xthl.wiki.data.ActionSource;
import org.xthl.wiki.data.ArticleSource;
import org.xthl.wiki.data.ArticleTextSource;
import org.xthl.wiki.data.RevisionSource;
import org.xthl.wiki.data.RevisionTextSource;
import org.xthl.wiki.data.CategoryArticleSource;

/**
 *
 * @author Alexey Andreev
 */
@ManagedBean(iface = ArticleLogic.class, scope = BeanScope.REQUEST)
public class ArticleLogicImpl implements ArticleLogic {
    private static final Logger logger = LoggerFactory.getLogger(ArticleLogicImpl.class);
    private DataManager dataManager;
    private LuceneProvider luceneProvider;
    private UserLogic userLogic;
    private QueryBuilder qb;
    
    @Injected
    public ArticleLogicImpl(DataManager dataManager, LuceneProvider luceneProvider,
            UserLogic userLogic) {
        this.dataManager = dataManager;
        this.luceneProvider = luceneProvider;
        this.userLogic = userLogic;
        this.qb = dataManager.getQueryBuilder();
    }

    @Override
    public Article get(int id) {
        if (logger.isDebugEnabled()) {
            logger.debug("Retrieving article #{}", id);
        }
        ArticleSource article = qb.get(ArticleSource.class);
        RevisionSource lastRev = qb.get(RevisionSource.class);
        DataResult result = dataManager.exec(qb.with(article)
                .join(lastRev, lastRev.id().eq(article.lastRevisionId()))
                .filter(article.id().eq(id))
                .fetch(article.id(), article.creationDate(), article.lastPublishedRevisionId(),
                    article.publicationDate(), article.deleted(), article.title(),
                    article.lastRevisionId(), lastRev.number()));
        if (!result.next()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Article #{} not found", id);
            }
            return null;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Returning article #{}", id);
        }
        return map(result);
    }
    
    private Article map(DataResult result) {
        Article dto = new Article();
        dto.setId(result.getInt(1));
        dto.setCreationDate(result.getDate(2));
        int lastPublishedRevisionId = result.getInt(3);
        dto.setLastPublishedRevisionId(lastPublishedRevisionId != 0 ?
                lastPublishedRevisionId : null);
        dto.setPublicationDate(result.getDate(4));
        dto.setDeleted(result.getBoolean(5));
        dto.setTitle(result.getString(6));
        dto.setLastRevisionId(result.getInt(7));
        dto.setLastRevisionNumber(result.getInt(8));
        return dto;
    }

    @Override
    public String getText(int articleId) {
        ArticleTextSource articleText = qb.get(ArticleTextSource.class);
        DataResult result = dataManager.exec(qb.with(articleText)
                .filter(articleText.articleId().eq(articleId))
                .fetch(articleText.text()));
        if (!result.next()) {
            throw new IllegalArgumentException("Article not found: " + articleId);
        }
        return result.getString(1);
    }
    
    private List<String> applyDiff(List<String> orig, List<String> diff) {
        List<String> targetLines = new ArrayList<String>();
        int index = 0;
        int lastCopied = 0;
        while (index < diff.size()) {
            String diffLine = diff.get(index);
            ++index;
            String parts[] = diffLine.split(",", 3);
            int start = Integer.parseInt(parts[0]);
            int len = Integer.parseInt(parts[1]);
            int newLen = Integer.parseInt(parts[2]);
            targetLines.addAll(orig.subList(lastCopied, start));
            targetLines.addAll(diff.subList(index, index + newLen));
            lastCopied = start + len;
            index += newLen;
        }
        targetLines.addAll(orig.subList(lastCopied, orig.size()));
        return targetLines;
    }

    @Override
    public String getRevisionText(int revisionId) {
        if (logger.isDebugEnabled()) {
            logger.debug("Fetching article id and revision number for revision #{}",
                    revisionId);
        }
        RevisionSource rev = qb.get(RevisionSource.class);
        DataResult result = dataManager.exec(qb.with(rev)
                .filter(rev.id().eq(revisionId))
                .fetch(rev.number(), rev.articleId()));
        if (!result.next()) {
            throw new IllegalArgumentException("Article revision not found: " + revisionId);
        }
        int revisionNumber = result.getInt(1);
        int articleId = result.getInt(2);
        if (logger.isDebugEnabled()) {
            logger.debug("Article id and revision number are {} and {}", articleId, revisionNumber);
        }
        return joinLines(getRevisionLines(articleId, revisionNumber));
    }
    
    private List<String> getRevisionLines(int articleId, int revisionNumber) {
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionTextSource revText = qb.get(RevisionTextSource.class);
        DataResult result = dataManager.exec(qb.with(rev)
                .join(revText, revText.revisionId().eq(rev.id()))
                .filter(rev.articleId().eq(articleId))
                .filter(rev.number().le(revisionNumber))
                .sortAsc(rev.number())
                .fetch(revText.text(), rev.number()));
        List<String> text = new ArrayList<String>();
        while (result.next()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Applying patch #{} to article #{}", result.getInt(2), articleId);
            }
            text = applyDiff(text, splitToLines(result.getString(1)));
        }
        return text;
    }
    
    private ExprBuilder getDeletedFilter(QueryBuilder qb, DeletedObjectFilter filter,
            ExprBuilder field) {
        switch (filter) {
            case DELETED_ONLY:
                return field.isTrue();
            case EXISTING_ONLY:
                return field.isFalse();
            default:
                return qb.getTrue();
        }
    }

    @Override
    public List<Article> find(ArticleSearchCritery critery) {
        switch (critery.getDeletedFilter()) {
            case ALL:
            case DELETED_ONLY:
                userLogic.requirePermission("wiki.listDeletedArticles");
                break;
        }
        ArticleSource article = qb.get(ArticleSource.class);
        CategoryArticleSource catArt = qb.get(CategoryArticleSource.class);
        RevisionSource lastRev = qb.get(RevisionSource.class);
        SelectBuilder src = qb.with(article)
                .join(lastRev, lastRev.id().eq(article.lastRevisionId()));
        if (critery.getCategoryId() != null) {
            src.join(catArt, catArt.articleId().eq(article.id()));
        }
        DataResult result = dataManager.exec(src
                .filter(critery.getStartDate() != null ? article.creationDate().ge(
                        critery.getStartDate()) : qb.getTrue())
                .filter(critery.getEndDate() != null ? article.creationDate().lt(
                        critery.getEndDate()) : qb.getTrue())
                .filter(critery.isPublishedOnly() ? article.publicationDate().isNotNull() : 
                        qb.getTrue())
                .filter(critery.getCategoryId() != null ? catArt.articleId().eq(
                        critery.getCategoryId()) : qb.getTrue())
                .filter(getDeletedFilter(qb, critery.getDeletedFilter(), article.deleted()))
                .sortDesc(article.creationDate())
                .skip(critery.getSkip())
                .take(critery.getLimit())
                .fetch(article.id(), article.creationDate(), article.lastPublishedRevisionId(),
                    article.publicationDate(), article.deleted(), article.title(),
                    article.lastRevisionId(), lastRev.number()));
            
        List<Article> dtos = new ArrayList<Article>();
        while (result.next()) {
            dtos.add(map(result));
        }
        return dtos;
    }

    @Override
    public List<Article> findByContent(ArticleSearchByContentCritery critery) {
        try {
            org.apache.lucene.search.Query query =
                    new QueryParser(Version.LUCENE_30, "content", luceneProvider.getAnalyzer())
                    .parse(critery.getContentPart());
            TopDocs docs = luceneProvider.getSearcher().search(query,
                    critery.getOffset() + critery.getLimit());
            List<Integer> ids = new ArrayList<Integer>();
            final Map<Integer, Integer> positions = new HashMap<Integer, Integer>();
            int index = 0;
            int upper = Math.min(docs.scoreDocs.length, critery.getOffset() + critery.getLimit());
            for (ScoreDoc doc : Arrays.copyOfRange(docs.scoreDocs, critery.getOffset(), upper)) {
                Document document = luceneProvider.getSearcher().doc(doc.doc);
                int id = Integer.parseInt(document.get("id"));
                positions.put(id, index++);
                ids.add(id);
            }
            ArticleSource article = qb.get(ArticleSource.class);
            RevisionSource lastRev = qb.get(RevisionSource.class);
            int batchSize = 100;
            List<Article> dtos = new ArrayList<Article>();
            for (int i = 0; i < ids.size(); i += batchSize) {
                int top = Math.min(ids.size(), i + batchSize);
                DataResult result = dataManager.exec(qb.with(article)
                        .join(lastRev, lastRev.id().eq(article.lastRevisionId()))
                        .filter(article.id().inNumbers(ids.subList(i, top)))
                        .fetch(article.id(), article.creationDate(),
                            article.lastPublishedRevisionId(), article.publicationDate(),
                            article.deleted(), article.title(), article.lastRevisionId(),
                            lastRev.number()));
                while (result.next()) {
                    dtos.add(map(result));
                }
            }
            Collections.sort(dtos, new Comparator<Article>() {
                @Override
                public int compare(Article o1, Article o2) {
                    return positions.get(o1.getId()).compareTo(positions.get(o2.getId()));
                }
            });
            return dtos;
        } catch (ParseException ex) {
            throw new IllegalArgumentException("Illegal search text");
        } catch (IOException ex) {
            throw new RuntimeException("Failed searching");
        }
    }

    @Override
    public int create(ArticleCreationCommand cmd) {
        ArticleSource article = qb.get(ArticleSource.class);
        ArticleTextSource articleText = qb.get(ArticleTextSource.class);
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionTextSource revText = qb.get(RevisionTextSource.class);
        ActionSource action = qb.get(ActionSource.class);
        
        int articleId = dataManager.nextInt(ArticleSource.ID_SEQUENCE);
        int revId = dataManager.nextInt(RevisionSource.ID_SEQUENCE);
        int actionId = dataManager.nextInt(ActionSource.ID_SEQUENCE);
        int userId = userLogic.getCurrentUser().getId();
        Date now = new Date();
        
        dataManager.exec(qb.insertInto(article)
                .field(article.id(), articleId)
                .field(article.creationDate(), now)
                .field(article.publicationDate(), qb.getNull())
                .field(article.deleted(), false)
                .field(article.lastPublishedRevisionId(), qb.getNull())
                .field(article.lastRevisionId(), revId)
                .field(article.title(), cmd.getTitle()));
        dataManager.exec(qb.insertInto(articleText)
                .field(articleText.articleId(), articleId)
                .field(articleText.text(), ""));
        
        dataManager.exec(qb.insertInto(rev)
                .field(rev.id(), revId)
                .field(rev.articleId(), articleId)
                .field(rev.authorId(), userId)
                .field(rev.date(), now)
                .field(rev.title(), cmd.getTitle())
                .field(rev.previousRevisionId(), qb.getNull())
                .field(rev.number(), 0));
        dataManager.exec(qb.insertInto(revText)
                .field(revText.revisionId(), revId)
                .field(revText.text(), ""));
        
        dataManager.exec(qb.insertInto(action)
                .field(action.id(), actionId)
                .field(action.date(), now)
                .field(action.type(), "created")
                .field(action.userId(), userId)
                .field(action.articleId(), articleId)
                .field(action.revisionId(), revId));
        
        return articleId;
    }
    
    private List<String> splitToLines(String text) {
        if (text.isEmpty()) {
            return Collections.emptyList();
        }
        return Arrays.asList(text.split("\\r\\n|\\r|\\n", -1));
    }
    
    private String joinLines(List<String> lines) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < lines.size(); ++i) {
            if (i > 0) {
                sb.append("\n");
            }
            sb.append(lines.get(i));
        }
        return sb.toString();
    }

    @Override
    public int edit(ArticleUpdateCommand cmd, Integer mergeRevisionId)
            throws ArticleConflictException {
        ArticleSource article = qb.get(ArticleSource.class);
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionTextSource revText = qb.get(RevisionTextSource.class);
        ActionSource action = qb.get(ActionSource.class);
        
        if (logger.isDebugEnabled()) {
            logger.debug("Checking whether previous revision ({}) exists",
                    cmd.getPreviousRevisionId());
        }
        DataResult result = dataManager.exec(qb.with(rev)
                .filter(rev.id().eq(cmd.getPreviousRevisionId()))
                .fetch(rev.articleId(), rev.number()));
        if (!result.next()) {
            throw new IllegalArgumentException("Revision not found: " +
                    cmd.getPreviousRevisionId());
        }
        int articleId = result.getInt(1);
        int revisionNumber = result.getInt(2);
        if (logger.isDebugEnabled()) {
            logger.debug("Previous revision ({}) found. Article {}, number {}",
                    new Object[] { cmd.getPreviousRevisionId(), articleId, revisionNumber });
        }
        int authorId = userLogic.getCurrentUser().getId();
        
        if (logger.isDebugEnabled()) {
            logger.debug("Checking for conflicts");
        }
        result = dataManager.exec(qb.with(rev)
                .filter(rev.previousRevisionId().eq(cmd.getPreviousRevisionId()))
                .fetch(rev.id(), rev.articleId(), rev.number()));
        if (result.next()) {
            throw new ArticleConflictException(result.getInt(2), cmd.getPreviousRevisionId(),
                    result.getInt(1));
        }
        List<String> oldLines = getRevisionLines(articleId, revisionNumber);
        List<String> newLines = splitToLines(cmd.getContent());
        String diff = joinLines(makeDiff(oldLines, newLines));
        Date now = new Date();
        int revisionId = dataManager.nextInt(RevisionSource.ID_SEQUENCE);
        int actionId = dataManager.nextInt(ActionSource.ID_SEQUENCE); 
        
        dataManager.exec(qb.with(article)
                .filter(article.id().eq(articleId))
                .set(article.lastRevisionId(), revisionId));
        dataManager.exec(qb.insertInto(rev)
                .field(rev.id(), revisionId)
                .field(rev.articleId(), articleId)
                .field(rev.authorId(), authorId)
                .field(rev.date(), now)
                .field(rev.mergedRevisionId(), mergeRevisionId)
                .field(rev.note(), cmd.getNote())
                .field(rev.number(), revisionNumber + 1)
                .field(rev.previousRevisionId(), cmd.getPreviousRevisionId())
                .field(rev.title(), cmd.getTitle()));
        dataManager.exec(qb.insertInto(revText)
                .field(revText.revisionId(), revisionId)
                .field(revText.text(), diff));
        
        dataManager.exec(qb.insertInto(action)
                .field(action.id(), actionId)
                .field(action.date(), now)
                .field(action.type(), "modified")
                .field(action.userId(), authorId)
                .field(action.articleId(), articleId)
                .field(action.revisionId(), revisionId));
        
        return revisionId;
    }
    
    private List<String> makeDiff(List<String> oldLines, List<String> newLines) {
        List<String> lines = new ArrayList<String>();
        Map<String, Integer> indexes = new HashMap<String, Integer>();
        int[] oldSequence = new int[oldLines.size()];
        int[] newSequence = new int[newLines.size()];
        int i = 0;
        for (String line : oldLines) {
            Integer index = indexes.get(line);
            if (index == null) {
                index = lines.size();
                lines.add(line);
                indexes.put(line, index);
            }
            oldSequence[i++] = index;
        }
        i = 0;
        for (String line : newLines) {
            Integer index = indexes.get(line);
            if (index == null) {
                index = lines.size();
                lines.add(line);
                indexes.put(line, index);
            }
            newSequence[i++] = index;
        }
        DeltaEncoder encoder = new DeltaEncoder();
        List<Change> changes = encoder.diff(oldSequence, newSequence);
        List<String> result = new ArrayList<String>();
        for (Change change : changes) {
            result.add(change.getStart() + "," + change.getLength() + "," + 
                    change.getDataLength());
            for (int index : change.getData()) {
                result.add(lines.get(index));
            }
        }
        return result;
    }
    
    @Override
    public void publish(int revisionId) {
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionSource pubRev = qb.get(RevisionSource.class);
        ArticleSource article = qb.get(ArticleSource.class);
        ArticleTextSource articleText = qb.get(ArticleTextSource.class);
        ActionSource action = qb.get(ActionSource.class);
        if (logger.isDebugEnabled()) {
            logger.debug("Checking number for revision #{}", revisionId);
        }
        DataResult result = dataManager.exec(qb.with(rev)
                .join(article, article.id().eq(rev.articleId()))
                .leftJoin(pubRev, pubRev.id().eq(article.lastPublishedRevisionId()))
                .filter(rev.id().eq(revisionId))
                .fetch(rev.number(), pubRev.number(), rev.articleId(), rev.title()));
        if (!result.next()) {
            throw new IllegalArgumentException("Revision #" + revisionId + " not found");
        }
        int currNum = result.getInt(1);
        int prevNum = result.getInt(2);
        int articleId = result.getInt(3);
        String title = result.getString(4);
        if (logger.isDebugEnabled()) {
            logger.debug("Article id is {}. Current revision is {}, previous is {}",
                    new Object[] { articleId, currNum, prevNum });
        }
        if (currNum <= prevNum) {
            throw new IllegalArgumentException("Revision #" + revisionId + " has number " +
                    currNum + " that is not greater than " + prevNum);
        }
        
        if (logger.isDebugEnabled()) {
            logger.debug("Updating search index");
        }
        String text = getRevisionText(revisionId);
        Document doc = new Document();
        doc.add(new Field("content", text, Field.Store.YES, Field.Index.ANALYZED));
        doc.add(new Field("id", String.valueOf(articleId), Field.Store.YES,
                Field.Index.NOT_ANALYZED));
        Term term = new Term("id", String.valueOf(articleId));
        try {
            luceneProvider.getWriter().updateDocument(term, doc);
            luceneProvider.getWriter().commit();
        } catch (IOException ex) {
            throw new RuntimeException("IO error occured updating index", ex);
        }
        
        if (logger.isDebugEnabled()) {
            logger.debug("Update database");
        }
        int actionId = dataManager.nextInt(ActionSource.ID_SEQUENCE);
        int userId = userLogic.getCurrentUser().getId();
        Date now = new Date();
        
        dataManager.exec(qb.with(article)
                .filter(article.id().eq(articleId))
                .set(article.lastPublishedRevisionId(), revisionId)
                .set(article.title(), title)
                .set(article.publicationDate(), now));
        dataManager.exec(qb.with(articleText)
                .filter(articleText.articleId().eq(articleId))
                .set(articleText.text(), text));
        dataManager.exec(qb.insertInto(action)
                .field(action.id(), actionId)
                .field(action.date(), now)
                .field(action.userId(), userId)
                .field(action.type(), "published")
                .field(action.articleId(), articleId)
                .field(action.revisionId(), revisionId));
    }

    @Override
    public Revision getRevision(int id) {
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionSource prevRev = qb.get(RevisionSource.class);
        ArticleSource article = qb.get(ArticleSource.class);
        UserSource author = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(rev)
                .leftJoin(prevRev, prevRev.id().eq(rev.previousRevisionId()))
                .join(author, author.id().eq(rev.authorId()))
                .join(article, article.id().eq(rev.articleId()))
                .filter(rev.id().eq(id))
                .fetch(rev.id(), rev.articleId(), article.title(), rev.authorId(), author.login(),
                    rev.date(), rev.previousRevisionId(), prevRev.number(), rev.mergedRevisionId(),
                    rev.number(), rev.note(), rev.title()));
        if (!result.next()) {
            return null;
        }
        return mapRevision(result);
    }

    @Override
    public Revision getRevisionByNumber(int articleId, int number) {
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionSource prevRev = qb.get(RevisionSource.class);
        ArticleSource article = qb.get(ArticleSource.class);
        UserSource author = qb.get(UserSource.class);
        DataResult result = dataManager.exec(qb.with(rev)
                .leftJoin(prevRev, prevRev.id().eq(rev.previousRevisionId()))
                .join(author, author.id().eq(rev.authorId()))
                .join(article, article.id().eq(rev.articleId()))
                .filter(rev.articleId().eq(articleId))
                .filter(rev.number().eq(number))
                .fetch(rev.id(), rev.articleId(), article.title(), rev.authorId(), author.login(),
                    rev.date(), rev.previousRevisionId(), prevRev.number(), rev.mergedRevisionId(),
                    rev.number(), rev.note(), rev.title()));
        if (!result.next()) {
            return null;
        }
        return mapRevision(result);
    }

    private Revision mapRevision(DataResult result) {
        Revision dto = new Revision();
        dto.setId(result.getInt(1));
        dto.setArticleId(result.getInt(2));
        dto.setArticleTitle(result.getString(3));
        dto.setAuthorId(result.getInt(4));
        dto.setAuthorName(result.getString(5));
        dto.setDate(result.getDate(6));
        int prevRevId = result.getInt(7);
        dto.setPreviousRevisionId(prevRevId != 0 ? prevRevId : null);
        dto.setPreviousRevisionNumber(prevRevId != 0 ? result.getInt(8) : null);
        int mergedRevId = result.getInt(9);
        dto.setMergedRevisionId(mergedRevId != 0 ? mergedRevId : null);
        dto.setNumber(result.getInt(10));
        dto.setNote(result.getString(11));
        dto.setTitle(result.getString(12));
        return dto;
    }

    @Override
    public List<Revision> findRevisions(RevisionSearchCritery critery) {
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionSource prevRev = qb.get(RevisionSource.class);
        ArticleSource article = qb.get(ArticleSource.class);
        UserSource author = qb.get(UserSource.class);
        List<Revision> dtos = new ArrayList<Revision>();
        DataResult result = dataManager.exec(qb.with(rev)
                .leftJoin(prevRev, prevRev.id().eq(rev.previousRevisionId()))
                .join(author, author.id().eq(rev.authorId()))
                .join(article, article.id().eq(rev.articleId()))
                .filter(critery.getArticleId() != null ?
                    rev.articleId().eq(critery.getArticleId()) : qb.getTrue())
                .filter(critery.getAuthorId() != null ?
                    rev.authorId().eq(critery.getAuthorId()) : qb.getTrue())
                .filter(critery.getStartDate() != null ?
                    rev.date().ge(critery.getStartDate()) : qb.getTrue())
                .filter(critery.getEndDate() != null ?
                    rev.date().lt(critery.getEndDate()) : qb.getTrue())
                .filter(critery.getFirstNumber() != null ?
                    rev.number().ge(critery.getFirstNumber()) : qb.getTrue())
                .filter(critery.getLastNumber() != null ?
                    rev.number().lt(critery.getLastNumber()) : qb.getTrue())
                .filter(getDeletedFilter(qb, critery.getDeletedFilter(), article.deleted()))
                .sortDesc(rev.date())
                .skip(critery.getSkip())
                .take(critery.getLimit())
                .fetch(rev.id(), rev.articleId(), article.title(), rev.authorId(), author.login(),
                    rev.date(), rev.previousRevisionId(), prevRev.number(),rev.mergedRevisionId(),
                    rev.number(), rev.note(), rev.title()));
        while (result.next()) {
            dtos.add(mapRevision(result));
        }
        return dtos;
    }

    @Override
    public void delete(int articleId) {
        ArticleSource article = qb.get(ArticleSource.class);
        ActionSource action = qb.get(ActionSource.class);
        DataResult result = dataManager.exec(qb.with(article)
                .filter(article.id().eq(articleId))
                .fetch(article.deleted()));
        if (!result.next()) {
            throw new IllegalArgumentException("Article #" + articleId + " does not exist");
        }
        if (result.getBoolean(1)) {
            if (logger.isInfoEnabled()) {
                logger.info("Article is already deleted");
            }
            return;
        }
        int userId = userLogic.getCurrentUser().getId();
        dataManager.exec(qb.with(article)
                .filter(article.id().eq(articleId))
                .set(article.deleted(), true));
       
        int actionId = dataManager.nextInt(ActionSource.ID_SEQUENCE);
        dataManager.exec(qb.insertInto(action)
                .field(action.id(), actionId)
                .field(action.userId(), userId)
                .field(action.date(), new Date())
                .field(action.articleId(), articleId)
                .field(action.type(), "deleted"));
        
        Term term = new Term("id", String.valueOf(articleId));
        try {
            luceneProvider.getWriter().deleteDocuments(term);
            luceneProvider.getWriter().commit();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public void restore(int articleId) {
        ArticleSource article = qb.get(ArticleSource.class);
        ArticleTextSource articleText = qb.get(ArticleTextSource.class);
        ActionSource action = qb.get(ActionSource.class);
        DataResult result = dataManager.exec(qb.with(article)
                .join(articleText, articleText.articleId().eq(article.id()))
                .filter(article.id().eq(articleId))
                .fetch(article.deleted(), article.lastPublishedRevisionId().isNotNull(),
                        articleText.text()));
        if (!result.next()) {
            throw new IllegalArgumentException("Article #" + articleId + " does not exist");
        }
        if (!result.getBoolean(1)) {
            if (logger.isInfoEnabled()) {
                logger.info("Article is not deleted yet");
            }
            return;
        }
        boolean published = result.getBoolean(2);
        String text = published ? result.getString(3) : null;
        int userId = userLogic.getCurrentUser().getId();
        dataManager.exec(qb.with(article)
                .filter(article.id().eq(articleId))
                .set(article.deleted(), false));
        
        int actionId = dataManager.nextInt(ActionSource.ID_SEQUENCE);
        dataManager.exec(qb.insertInto(action)
                .field(action.id(), actionId)
                .field(action.userId(), userId)
                .field(action.date(), new Date())
                .field(action.articleId(), articleId)
                .field(action.type(), "restored"));
        if (published) {
            Term term = new Term("id", String.valueOf(articleId));
            Document doc = new Document();
            doc.add(new Field("content", text, Field.Store.YES, Field.Index.ANALYZED));
            doc.add(new Field("id", String.valueOf(articleId), Field.Store.YES,
                    Field.Index.NOT_ANALYZED));
            try {
                luceneProvider.getWriter().updateDocument(term, doc);
                luceneProvider.getWriter().commit();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    @Override
    public void rebuildIndex() throws RestrictedAccessException {
        ArticleSource article = qb.get(ArticleSource.class);
        ArticleTextSource articleText = qb.get(ArticleTextSource.class);
        try {
            luceneProvider.getWriter().deleteAll();
            int lastId = 0;
            while (true) {
                DataResult result = dataManager.exec(qb.with(article)
                        .join(articleText, articleText.articleId().eq(article.id()))
                        .filter(article.lastPublishedRevisionId().isNotNull())
                        .filter(article.deleted().isFalse())
                        .filter(article.id().gt(lastId))
                        .sortAsc(article.id())
                        .take(1000)
                        .fetch(article.id(), articleText.text()));
                boolean hasOne = false;
                while (result.next()) {
                    hasOne = true;
                    lastId = result.getInt(1);
                    String text = result.getString(2);
                    Document doc = new Document();
                    doc.add(new Field("content", text, Field.Store.YES, Field.Index.ANALYZED));
                    doc.add(new Field("id", String.valueOf(lastId), Field.Store.YES,
                            Field.Index.NOT_ANALYZED));
                    luceneProvider.getWriter().addDocument(doc);
                }
                if (!hasOne) {
                    break;
                }
            }
            luceneProvider.getWriter().commit();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public String getDiff(int firstRevisionId, int lastRevisionId) {
        RevisionSource rev = qb.get(RevisionSource.class);
        RevisionTextSource revText = qb.get(RevisionTextSource.class);
        DataResult result = dataManager.exec(qb.with(rev)
                .filter(rev.id().eq(firstRevisionId))
                .fetch(rev.articleId(), rev.number()));
        if (!result.next()) {
            throw new IllegalArgumentException("Revision not found: " + firstRevisionId);
        }
        int firstArticleId = result.getInt(1);
        int firstNum = result.getInt(2);
        result = dataManager.exec(qb.with(rev)
                .filter(rev.id().eq(lastRevisionId))
                .fetch(rev.articleId(), rev.number()));
        if (!result.next()) {
            throw new IllegalArgumentException("Revision not found: " + lastRevisionId);
        }
        int lastArticleId = result.getInt(1);
        int lastNum = result.getInt(2);
        if (firstArticleId != lastArticleId) {
            throw new IllegalArgumentException("Revisions " + firstRevisionId + " and " +
                    lastRevisionId + " are from different articles: ");
        }
        if (lastNum <= firstNum) {
            throw new IllegalArgumentException("Last revision number is not greater than " +
            		"first revision number");
        }
        
        result = dataManager.exec(qb.with(rev)
                .join(revText, revText.revisionId().eq(rev.id()))
                .filter(rev.articleId().eq(firstArticleId))
                .filter(rev.number().gt(firstNum))
                .filter(rev.number().le(lastNum))
                .sortAsc(rev.number())
                .fetch(revText.text()));
        if (lastNum == firstNum + 1) {
            result.next();
            return result.getString(1);
        } else {
            List<Change> resultPatch = new ArrayList<Change>();
            Map<String, Integer> indexes = new HashMap<String, Integer>();
            List<String> strings = new ArrayList<String>();
            while (result.next()) {
                List<String> diff = splitToLines(result.getString(1));
                List<Change> patch = new ArrayList<Change>();
                int index = 0;
                while (index < diff.size()) {
                    String diffLine = diff.get(index);
                    ++index;
                    String parts[] = diffLine.split(",", 3);
                    int start = Integer.parseInt(parts[0]);
                    int len = Integer.parseInt(parts[1]);
                    int newLen = Integer.parseInt(parts[2]);
                    int[] data = new int[newLen];
                    for (int i = 0; i < newLen; ++i) {
                        diffLine = diff.get(index++);
                        Integer elem = indexes.get(diffLine);
                        if (elem == null) {
                            elem = strings.size();
                            indexes.put(diffLine, elem);
                            strings.add(diffLine);
                        }
                        data[i] = elem;
                    }
                    patch.add(Change.replacement(start, len, data));
                }
                resultPatch = Patcher.compose(resultPatch, patch);
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < resultPatch.size(); ++i) {
                if (i > 0) {
                    sb.append('\n');
                }
                Change change = resultPatch.get(i);
                sb.append(change.getStart() + "," + change.getLength() + "," +
                        change.getDataLength());
                for (int item : change.getData()) {
                    sb.append('\n');
                    sb.append(strings.get(item));
                }
            }
            return sb.toString();
        }
    }
}
